package org.apache.lucene.store;

import static org.apache.lucene.store.GAELuceneConstants.fileBlobPropertyPrefixName;
import static org.apache.lucene.store.GAELuceneStaticMethods.attemptCommit;
import static org.apache.lucene.store.GAELuceneStaticMethods.buildDirectoryKey;
import static org.apache.lucene.store.GAELuceneStaticMethods.buildDirectoryVersionKey;
import static org.apache.lucene.store.GAELuceneStaticMethods.buildEntities;
import static org.apache.lucene.store.GAELuceneStaticMethods.checkTransaction;
import static org.apache.lucene.store.GAELuceneStaticMethods.getDirectoryEntities;
import static org.apache.lucene.store.GAELuceneStaticMethods.getDirectorySpansEntities;
import static org.apache.lucene.store.GAELuceneStaticMethods.getDirectoryVersion;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFileCount;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFileLastModifyDate;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFileLength;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFileLocationReferenceCount;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFileLocationReferenceEntityKey;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFileLocationReferenceSectionSize;
import static org.apache.lucene.store.GAELuceneStaticMethods.getFilename;
import static org.apache.lucene.store.GAELuceneStaticMethods.readEntitiesFromPersistentStore;
import static org.apache.lucene.store.GAELuceneStaticMethods.readEntityFromPersistentStore;
import static org.apache.lucene.store.GAELuceneStaticMethods.setDirectoryVersion;
import static org.apache.lucene.store.GAELuceneStaticMethods.writeEntitiesToPersistentStore;
import static org.apache.lucene.store.GAELuceneStaticMethods.writeEntityToPersistentStore;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

/**
 * @class GAEDirectory is a hybrid memory/persistent storage directory.
 * 
 * When the first file access is attempted, the directory will
 * try to access the file manifest from the MemCache Service, then from
 * the DataStore Service. Future references to these two services will be
 * call persistent storage, as all writes will go to both and reads will attempt
 * MemCache then DataStore. 
 * 
 * To ensure a consistent index exists, all changes will be read and written
 * to persistent storage together inside a single transaction.
 * 
 * The @see pullLatest() method will compare the cached file manifest version
 * number against what is currently in persistent storage. If they match,
 * the in memory files are left as is. If they don't match, the new files from
 * persistent storage will be pulled and put into memory for access. 
 * 
 * Deletes will not be checked during a pull so that from the perspective of any active @link IndexReaders
 * they will appear to continue existing, therefore achieving the "delete after last
 * close" method. They will however push the delete to persistent storage.
 * 
 * This method will be called automatically after successfully acquiring a lock.
 * 
 * @author Joel Wilson
 *
 */
public class GAERAMDirectory extends RAMDirectory implements GAEDirectoryPersister {
	
	private static final long serialVersionUID = 1l;
	private static final Logger logger = Logger.getLogger(GAERAMDirectory.class.getSimpleName());
	
	// important variables
	protected boolean useMemcache = true;
	protected final String directoryName; // name of the directory, allows multiple directories in the same DataStore "Kind"
	protected final Key directoryEntityKey; // key used for accessing the data in their datastoreService or memcacheService
	protected final Key directoryVersionEntityKey; // keys use for accessing directory version information and directorySpansEntities information
	volatile protected long version = 0l; // version of the directory this instance is serving
	
	public GAERAMDirectory(String directoryName) {
		this(directoryName, true);
	}
	
	public GAERAMDirectory(String directoryName, boolean preload, Key parentKey) {
		this.directoryName = directoryName;
		setLockFactory(new DatastoreLockFactory(parentKey, directoryName));
		directoryEntityKey = buildDirectoryKey(parentKey, this);
		directoryVersionEntityKey = buildDirectoryVersionKey(directoryEntityKey);
		if (preload)
			try {
				pullLatest();
			} catch (IOException ioException) {
				logger.warning("IOException while trying to initialize the GAERAMDirectory instance.\n" + ioException.getMessage());
				logger.warning(ioException.getMessage());
				throw new RuntimeException("IOException while trying to initialize the GAERAMDirectory instance.", ioException);
			}
	}
	
	public GAERAMDirectory(String directoryName, boolean preload) {
		this(directoryName, preload, null);
	}

	@Override
	public Lock makeLock(String name) {
		if (lockFactory instanceof DatastoreLockFactory) {
			return ((DatastoreLockFactory) lockFactory).makeLock(name, this);
		} else		
			return lockFactory.makeLock(name);
	}

	@Override
	public String getLockID() {
		return directoryName;
	} 
	
	public synchronized void pushContent() throws IOException {
		pushContent(null);
	}
	
	public synchronized void pushContent(Transaction trx) throws IOException {
		// build the entity that will store the files
		List<Entity> directoryEntities = buildEntities(this);
		// determine and set the version
		long newVersion = version + 1l;
		setDirectoryVersion(directoryEntities.get(0), newVersion);
		setDirectoryVersion(directoryEntities.get(1), newVersion);
		
		// initiate the transaction
		boolean autonomousTransaction = (trx == null || !trx.isActive());
		
		try {
			if (autonomousTransaction)
				trx = DatastoreServiceFactory.getDatastoreService().beginTransaction();
			
			// write the entities to persistent storage
			if (directoryEntities.size() == 2) 
				writeEntitiesToPersistentStore(directoryEntities, useMemcache, trx, true);
			else {
				// if the directory is set to use memcache and there are more than two entities, we only want to
				// put the directoryVersionEntity into memcache because we don't try to pull multiple entities
				// from memcache because it isn't an atomic read
				if (useMemcache && directoryEntities.size() > 2)
					writeEntityToPersistentStore(directoryEntities.get(1), true, trx);
				writeEntitiesToPersistentStore(directoryEntities, false, trx, false);
				}
			
				if (autonomousTransaction)
					attemptCommit(trx);
			} finally {
				if (autonomousTransaction)
					checkTransaction(trx);
			}
		// increment the database version
		version = newVersion;
	}
	
	public synchronized boolean pullLatest() throws IOException {
		return pullLatest(null);
	}
	
	public synchronized boolean pullLatest(Transaction trx) throws IOException {
		Map<Key, Entity> directoryEntities = null;
		Entity directoryEntity = null;
		boolean updatesToMake = false;
		boolean madeUpdates = false;
		Long directoryVersion = null;
		boolean autonomousTransaction = (trx == null || !trx.isActive());
		
		try {
			if (autonomousTransaction)
				trx = DatastoreServiceFactory.getDatastoreService().beginTransaction();
			
			// read the version entity from memcache
			Entity directoryVersionEntity = readEntityFromPersistentStore(directoryVersionEntityKey, useMemcache, trx);
			if (directoryVersionEntity != null) {
				directoryVersion = getDirectoryVersion(directoryVersionEntity);
				if (directoryVersion != null && !directoryVersion.equals(version)) {
					// mark the we have updates to make because the version is outdated
					updatesToMake = true;
					// read the directory entity itself
					directoryEntity = readEntityFromPersistentStore(directoryEntityKey, useMemcache, trx);
					// if we read the directory without a transaction and the versions between the directoryVersionEntity
					// and the directoryEntity are equal, start a transaction and read the directoryEntity again
					Long directoryVersion2 = getDirectoryVersion(directoryVersionEntity);
					if (!directoryVersion2.equals(directoryVersion)) {
						trx = DatastoreServiceFactory.getDatastoreService().beginTransaction();
						directoryEntity = readEntityFromPersistentStore(directoryEntityKey, false, trx);
						directoryVersion = getDirectoryVersion(directoryEntity);
					}
					// read the other directory entities if there are any
					if (getDirectorySpansEntities(directoryEntity)) {
						directoryEntities = readEntitiesFromPersistentStore(getDirectoryEntities(directoryEntity), false, trx);
					}
				}
			}
			
			if (autonomousTransaction)
				attemptCommit(trx);
		} finally {
			if (autonomousTransaction)
				checkTransaction(trx);
		}
				
		if (updatesToMake) {
			try {
				clear();
				
				// put the files into the fileMap
				int fileCount = getFileCount(directoryEntity);
				for (int fileNumber = 1; fileNumber <= fileCount; fileNumber++) {
					String fileName = getFilename(directoryEntity, fileNumber);
					RAMFile file = new RAMFile(this);
					fileMap.put(fileName, file);
					
					// copy the values from the entities into the file object
					file.setLastModified(getFileLastModifyDate(directoryEntity, fileNumber));
					file.setLength(getFileLength(directoryEntity, fileNumber));
					RAMOutputStream fileWriter = new RAMOutputStream(file);
					
					int fileSections = getFileLocationReferenceCount(directoryEntity, fileNumber);
					for (int sectionNumber = 1; sectionNumber <= fileSections; sectionNumber++) {
						Key sectionKey = getFileLocationReferenceEntityKey(directoryEntity, fileNumber, sectionNumber);
						int sectionLength = getFileLocationReferenceSectionSize(directoryEntity, fileNumber, sectionNumber);
						Blob blob = null;
						
						if (sectionKey == null)
							blob = (Blob)directoryEntity.getProperty(fileBlobPropertyPrefixName + fileNumber);
						else
							blob = (Blob)directoryEntities.get(sectionKey).getProperty(fileBlobPropertyPrefixName + fileNumber);
						
						fileWriter.writeBytes(blob.getBytes(), sectionLength);
					}
				}
				
				version = directoryVersion;
				madeUpdates = true;
			} catch (RuntimeException runtimeException) {
				clear();
			} catch (IOException ioException) {
				clear();
			}
		}
		
		return madeUpdates;
	}
	
	private void clear() {
		// clear out the directory
		for (Map.Entry<String, RAMFile> fileEntry : fileMap.entrySet())
			fileEntry.getValue().directory = null;
		fileMap.clear();
		sizeInBytes = 0l;
		version = 0l;
	}

	public synchronized boolean isUseMemcache() {
		return useMemcache;
	}

	public synchronized void setUseMemcache(boolean useMemcache) {
		this.useMemcache = useMemcache;
	}
}
